﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClassifierLib
{
    /// <summary>
    /// This class represents a population of rules
    /// This allows overrides for handling numerosity when adding new rules
    /// This also perfoms rule culling etc
    /// </summary>
    public class Population : ICollection<Rule>, ICloneable
    {
        private List<Rule> _rules;

        public Population()
        {
            _rules = new List<Rule>();
        }
        public Population(int capacity)
        {
            _rules = new List<Rule>(capacity);
        }
        public Population(IEnumerable<Rule> collection)
        {
            _rules = new List<Rule>(collection);
        }

        public Rule this[int index]
        {
            get
            {
                return _rules[index];
            }
            set
            {
                _rules[index] = value;
            }
        }

        #region ICollection<Rule> Members

        /// <summary>
        /// Add to population when numerosity is not to be incremented
        /// </summary>
        /// <param name="item"></param>
        public void Add(Rule item) //new
        {
            bool unique = true;
            foreach (Rule rule in this)
            {
                if (rule.Condition.Equals(item.Condition) && rule.Action.Equals(item.Action))
                {
                    unique = false;
                    break;
                }
            }

            if (unique)
                _rules.Add(item);
        }

        /// <summary>
        /// Adds rule to population with numerosity support
        /// </summary>
        /// <param name="item"></param>
        public void AddIncNumerosity(Rule item)
        {
            bool unique = true;
            foreach (Rule rule in this)
            {
                if (rule.Condition.Equals(item.Condition) && rule.Action.Equals(item.Action))
                {
                    unique = false;
                    rule.Numerosity++;
                    break;
                }
            }

            if (unique)
                _rules.Add(item);
        }

        public void Clear()
        {
            _rules.Clear();
        }

        public bool Contains(Rule item)
        {
            return _rules.Contains(item);
        }

        public void CopyTo(Rule[] array, int arrayIndex)
        {
            _rules.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get
            {
                return _rules.Count;
            }
        }

        /// <summary>
        /// This value is the number of rules in the population, not the size of the underlying array
        /// (includes numerosity support)
        /// </summary>
        public double PopulationSize
        {
            get
            {
                double total = 0;
                foreach (Rule r in _rules)
                {
                    total += r.Numerosity;
                }
                return total;
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Removes Rule completely from Population set
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool Remove(Rule item)
        {
            return _rules.Remove(item);
        }

        /// <summary>
        /// Decrements Numerosity on Rule. If Numerosity less than or equal to 1 removes
        /// from population set
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool DecrementCount(Rule item)
        {
            if (item.Numerosity > 1)
            {
                item.Numerosity--;
                return true;    //Return value ??????
            }
            else
                return _rules.Remove(item);
        }

        #endregion


        #region IEnumerable<Rule> Members

        public IEnumerator<Rule> GetEnumerator()
        {
            return _rules.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _rules.GetEnumerator();
        }

        #endregion

        #region ICloneable Members

        public object Clone()
        {
            return new Population(new List<Rule>(_rules));
        }

        #endregion


        #region Specialist selection methods

        public Population RouletteWheel(int numberToSelect)
        {
            double[] wheel = new double[this.Count];

            Population result = new Population();

            double sum = 0;

            for (int i = 0; i < this.Count; i++)
            {
                double thisDelta = this[i].Statistics["fitness"] * this[i].Numerosity;
                sum += thisDelta;
                wheel[i] = thisDelta;
            }

            for (int i = 0; i < numberToSelect; i++)
            {
                double randomValue = PersistantRandom.Instance.NextDouble() * sum;
                double total = 0;

                for (int j = 0; j < wheel.Length; j++)
                {
                    total += wheel[j];
                    if (total > randomValue)
                    {
                        // add to result list
                        result.Add(this[j]);

                        // reduce sum by amount added previously
                        sum -= wheel[j];

                        // disable this element in wheel
                        wheel[j] = 0;

                        break;
                    }
                }
            }

            return result;
        }

        public Rule InverseRouletteWheel()
        {
            double[] wheel = new double[this.Count];
            Rule result = new Rule();

            double voteSum = 0;
            double average = 0;

            // calculate average fitness first
            for (int j = 0; j < this.Count; j++)
            {
                average += this[j].Numerosity * this[j].Statistics["fitness"];
            }

            // divide through by pop size, not count
            average /= this.PopulationSize;

            for (int j = 0; j < this.Count; j++)
            {
                voteSum += deletionVote(this[j], average);
            }

            double random = PersistantRandom.Instance.NextDouble();
            double choicePoint = random * voteSum;
            double oldVoteSum = voteSum;    //DEBUG
            voteSum = 0;

            for (int j = 0; j < this.Count; j++)
            {
                voteSum += deletionVote(this[j], average);
                if (voteSum > choicePoint)
                {
                    result = this[j];
                    break;
                }
            }

            return result;
        }

        /*
        public Population InverseRouletteWheel(double numberToSelect)
        {
            // DEPRECATED: please remove when new algorithm has been verified
            //double[] wheel = new double[this.Numerosity];
            //Population result = new Population();

            //double sum = 0;
            //double average = 0;

            //for (int i = 0; i < this.Numerosity; i++)
            //{
            //    average += this[i].Numerosity * this[i].Statistics["fitness"];
            //    sum += (this[i].Statistics["nicheSize"] * this[i].Numerosity + 0.001);
            //    wheel[i] = sum;
            //}

            //// divide through by pop size, not count
            //average /= this.PopulationSize;

            //while (true)
            //{
            //    for (int i = 0; i < numberToSelect; i++)
            //    {
            //        double randomValue = PersistantRandom.Instance.NextDouble() * sum;

            //        for (int j = 0; j < wheel.Length; j++)
            //        {
            //            if (wheel[j] > randomValue)
            //            {
            //                result.Add(this[j]);
            //                break;
            //            }
            //        }

            //    }
            //    if (result[0].Statistics["fitness"] <= average)
            //    {
            //        break;
            //    }
            //    else
            //    {
            //        result.Clear(); // throw out our values
            //    }
            //}
            //return result;



            ////////////This was Tom's implementation, will try improve, but here if need to revert back to it/////////////

            //double[] wheel = new double[this.Numerosity];
            //Population result = new Population();

            //double sum = 0;
            //double average = 0;

            //// calculate average fitness first
            //for (int i = 0; i < this.Numerosity; i++)
            //{
            //    average += this[i].Numerosity * this[i].Statistics["fitness"];
            //}

            //// divide through by pop size, not count
            //average /= this.PopulationSize;


            //// Will: the following should leave wheel entries as negative for rules that have better than average fitness, and low age
            //// the effect of this should be to ensure that these values can never be selected as they can never be greater then the randomValue generated
            //// I have traded an extra loop through the array for not having the while(1) logic
            //for (int i = 0; i < this.Numerosity; i++)
            //{
            //    //BPH InverseRouletteWheel wasn't returning any population so removed this temporarily
            //    //if ((this[i].Statistics["fitness"] > average) || (this[i].Statistics["age"] < 6))
            //    //{
            //    //    wheel[i] = 0;
            //    //    continue;
            //    //}

            //    double thisDelta = (this[i].Statistics["nicheSize"] * this[i].Numerosity + 0.001);
            //    sum += thisDelta;
            //    wheel[i] = thisDelta;
            //}


            //for (int i = 0; i < numberToSelect; i++)
            //{
            //    double randomValue = PersistantRandom.Instance.NextDouble() * sum;
            //    double total = 0;

            //    for (int j = 0; j < wheel.Length; j++)
            //    {
            //        total += wheel[j];
            //        if (total > randomValue)
            //        {
            //            result.Add(this[j]);

            //            sum -= wheel[j];

            //            wheel[j] = 0;

            //            break;
            //        }
            //    }

            //}

            //return result;



            double[] wheel = new double[this.Numerosity];
            Population result = new Population();

            for (int i = 0; i < numberToSelect; i++)
            {
                double voteSum = 0;
                double average = 0;

                // calculate average fitness first
                for (int j = 0; j < this.Numerosity; j++)
                {
                    average += this[j].Numerosity * this[j].Statistics["fitness"];
                }

                // divide through by pop size, not count
                average /= this.PopulationSize;

                for (int j = 0; j < this.Numerosity; j++)
                {
                    voteSum += deletionVote(this[j], average);
                }

                double random = PersistantRandom.Instance.NextDouble();
                double choicePoint = random * voteSum;
                double oldVoteSum = voteSum;    //DEBUG
                voteSum = 0;

                for (int j = 0; j < this.Numerosity; j++)
                {
                    voteSum += deletionVote(this[j], average);
                    if (voteSum > choicePoint)
                    {
                        //BPH TODO: This is often/(all the time) adding the same rule, we need to be decreasing numerosity/deleting here not back in ExploreSelect
                        result.Add(this[j]);
                        break;
                    }
                }
            }

            return result;
        }*/

        double deletionVote(Rule rule, double averageFitness)
        {
            double vote;

            vote = rule.Statistics["nicheSize"] * rule.Numerosity;
            //TODO: Create Variables for 5 and 0.05
            if ((rule.Statistics["age"] > 5) && (rule.Statistics["fitness"] / rule.Numerosity < 0.05 * averageFitness))
                vote *= averageFitness / (rule.Statistics["fitness"] / rule.Numerosity);

            return vote;
        }

        #endregion
    }
}
